package io.kiki.stack.http.feign;

import io.kiki.stack.http.feign.Request.Options;

import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.stream.Stream;

import static io.kiki.stack.http.feign.ExceptionPropagationPolicy.UNWRAP;
import static io.kiki.stack.http.feign.FeignException.errorExecuting;
import static io.kiki.stack.http.feign.Util.checkNotNull;

final class AsynchronousMethodHandler<C> implements MethodHandler {

    private final AsyncClient<C> client;
    private final C requestContext;
    private final AsyncResponseHandler asyncResponseHandler;
    private final MethodInfo methodInfo;
    private final MethodHandlerConfiguration methodHandlerConfiguration;

    private AsynchronousMethodHandler(MethodHandlerConfiguration methodHandlerConfiguration, AsyncClient<C> client, AsyncResponseHandler asyncResponseHandler, C requestContext, MethodInfo methodInfo) {
        this.methodHandlerConfiguration = checkNotNull(methodHandlerConfiguration, "methodHandlerConfiguration");
        this.client = checkNotNull(client, "client for %s", methodHandlerConfiguration.getTarget());
        this.requestContext = requestContext;
        this.asyncResponseHandler = asyncResponseHandler;
        this.methodInfo = methodInfo;
    }

    private static Response ensureRequestIsSet(Response response, RequestTemplate requestTemplate, Request request) {
        return response.toBuilder().request(request).requestTemplate(requestTemplate).build();
    }

    @Override
    public Object invoke(Object[] argv) throws Throwable {
        RequestTemplate requestTemplate = methodHandlerConfiguration.getBuildTemplateFromArgs().create(argv);
        Options options = findOptions(argv);
        Retryer retryer = this.methodHandlerConfiguration.getRetryer().clone();
        try {
            if (methodInfo.isAsyncReturnType()) {
                return executeAndDecode(requestTemplate, options, retryer);
            } else {
                return executeAndDecode(requestTemplate, options, retryer).join();
            }
        } catch (CompletionException e) {
            throw e.getCause();
        }
    }

    private CompletableFuture<Object> executeAndDecode(RequestTemplate requestTemplate, Options options, Retryer retryer) {
        CancellableFuture<Object> cancellableFuture = new CancellableFuture<>();

        executeAndDecode(requestTemplate, options).whenComplete((response, throwable) -> {
            if (throwable != null) {
                if (!cancellableFuture.isDone() && shouldRetry(retryer, throwable, cancellableFuture)) {
                    if (methodHandlerConfiguration.getLogLevel() != Logger.Level.NONE) {
                        methodHandlerConfiguration.getLogger().logRetry(methodHandlerConfiguration.getMethodMetadata().configKey(), methodHandlerConfiguration.getLogLevel());
                    }

                    cancellableFuture.setInner(executeAndDecode(requestTemplate, options, retryer));
                }
            } else {
                cancellableFuture.complete(response);
            }
        });

        return cancellableFuture;
    }

    private boolean shouldRetry(Retryer retryer, Throwable throwable, CompletableFuture<Object> resultFuture) {
        if (throwable instanceof CompletionException) {
            throwable = throwable.getCause();
        }

        if (!(throwable instanceof RetryableException retryableException)) {
            resultFuture.completeExceptionally(throwable);
            return false;
        }

        try {
            retryer.continueOrPropagate(retryableException);
            return true;
        } catch (RetryableException th) {
            Throwable cause = th.getCause();
            if (methodHandlerConfiguration.getExceptionPropagationPolicy() == UNWRAP && cause != null) {
                resultFuture.completeExceptionally(cause);
            } else {
                resultFuture.completeExceptionally(th);
            }
            return false;
        }
    }

    private CompletableFuture<Object> executeAndDecode(RequestTemplate requestTemplate, Options options) {
        Request request = targetRequest(requestTemplate);

        if (methodHandlerConfiguration.getLogLevel() != Logger.Level.NONE) {
            methodHandlerConfiguration.getLogger().logRequest(methodHandlerConfiguration.getMethodMetadata().configKey(), methodHandlerConfiguration.getLogLevel(), request);
        }

        long start = System.nanoTime();
        return client.execute(request, options, Optional.ofNullable(requestContext)).thenApply(response ->
                // TODO: remove in Feign 12
                ensureRequestIsSet(response, requestTemplate, request)).exceptionally(throwable -> {
            CompletionException completionException = throwable instanceof CompletionException ? (CompletionException) throwable : new CompletionException(throwable);
            if (completionException.getCause() instanceof IOException ioException) {
                if (methodHandlerConfiguration.getLogLevel() != Logger.Level.NONE) {
                    methodHandlerConfiguration.getLogger().logIOException(methodHandlerConfiguration.getMethodMetadata().configKey(), methodHandlerConfiguration.getLogLevel(), ioException, elapsedTime(start));
                }

                throw errorExecuting(request, ioException);
            } else {
                throw completionException;
            }
        }).thenCompose(response -> handleResponse(response, elapsedTime(start)));
    }

    private CompletableFuture<Object> handleResponse(Response response, long elapsedTime) {
        return asyncResponseHandler.handleResponse(methodHandlerConfiguration.getMethodMetadata().configKey(), response, methodInfo.underlyingReturnType(), elapsedTime);
    }

    private long elapsedTime(long start) {
        return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
    }

    private Request targetRequest(RequestTemplate requestTemplate) {
        for (RequestInterceptor requestInterceptor : methodHandlerConfiguration.getRequestInterceptorList()) {
            requestInterceptor.apply(requestTemplate);
        }
        return methodHandlerConfiguration.getTarget().apply(requestTemplate);
    }

    private Options findOptions(Object[] argv) {
        if (argv == null || argv.length == 0) {
            return this.methodHandlerConfiguration.getOptions();
        }
        return Stream.of(argv).filter(Options.class::isInstance).map(Options.class::cast).findFirst().orElse(this.methodHandlerConfiguration.getOptions());
    }

    private static class CancellableFuture<T> extends CompletableFuture<T> {
        private CompletableFuture<T> inner = null;

        private static <T> BiConsumer<? super T, ? super Throwable> pipeTo(CompletableFuture<T> completableFuture) {
            return (value, throwable) -> {
                if (completableFuture.isDone()) {
                    return;
                }

                if (throwable != null) {
                    completableFuture.completeExceptionally(throwable);
                } else {
                    completableFuture.complete(value);
                }
            };
        }

        public void setInner(CompletableFuture<T> value) {
            inner = value;
            inner.whenComplete(pipeTo(this));
        }

        @Override
        public boolean cancel(boolean mayInterruptIfRunning) {
            final boolean result = super.cancel(mayInterruptIfRunning);
            if (inner != null) {
                inner.cancel(mayInterruptIfRunning);
            }
            return result;
        }
    }

    static class MethodHandlerFactory<C> implements io.kiki.stack.http.feign.MethodHandlerFactory<C> {

        private final AsyncClient<C> client;
        private final Retryer retryer;
        private final List<RequestInterceptor> requestInterceptors;
        private final AsyncResponseHandler responseHandler;
        private final Logger logger;
        private final Logger.Level logLevel;
        private final ExceptionPropagationPolicy propagationPolicy;
        private final MethodInfoResolver methodInfoResolver;
        private final RequestTemplateFactoryResolver requestTemplateFactoryResolver;
        private final Options options;

        MethodHandlerFactory(AsyncClient<C> client, Retryer retryer, List<RequestInterceptor> requestInterceptors, AsyncResponseHandler responseHandler, Logger logger, Logger.Level logLevel, ExceptionPropagationPolicy propagationPolicy, MethodInfoResolver methodInfoResolver, RequestTemplateFactoryResolver requestTemplateFactoryResolver, Options options) {
            this.client = checkNotNull(client, "client");
            this.retryer = checkNotNull(retryer, "retryer");
            this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors");
            this.responseHandler = responseHandler;
            this.logger = checkNotNull(logger, "logger");
            this.logLevel = checkNotNull(logLevel, "logLevel");
            this.propagationPolicy = propagationPolicy;
            this.methodInfoResolver = methodInfoResolver;
            this.requestTemplateFactoryResolver = checkNotNull(requestTemplateFactoryResolver, "requestTemplateFactoryResolver");
            this.options = checkNotNull(options, "options");
        }

        @Override
        public MethodHandler create(Target<?> target, MethodMetadata metadata, C requestContext) {
            final RequestTemplateFactory buildTemplateFromArgs = requestTemplateFactoryResolver.resolve(target, metadata);

            MethodHandlerConfiguration methodHandlerConfiguration = new MethodHandlerConfiguration(metadata, target, retryer, requestInterceptors, logger, logLevel, buildTemplateFromArgs, options, propagationPolicy);
            return new AsynchronousMethodHandler<C>(methodHandlerConfiguration, client, responseHandler, requestContext, methodInfoResolver.resolve(target.type(), metadata.method()));
        }
    }
}
